﻿// SKNotes - a note taking and gathering tool
// Copyright (C) 2009  Stefan Kueng
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace SKNotes
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.IO.Packaging;
    using System.Linq;
    using System.Runtime.Serialization.Formatters.Binary;
    using SKNotes.CustomControls;
    using SKNotes.CustomControls.TaskDialog;
    using SKNotes.Utilities;

    public class NoteList : BindableObject
    {
        private static readonly NoteList instance;
        private const string NoteSavePath = @"D:\Development\SKNotes\testfiles";

        private InvokeOC<NoteBook> tags;
        private InvokeOC<Note> notes;
        private NoteBook currentNoteBook;

        static NoteList()
        {
            instance = new NoteList();
        }

        protected NoteList()
        {
            this.tags = new InvokeOC<NoteBook>(App.Current.Dispatcher);
            this.notes = new InvokeOC<Note>(App.Current.Dispatcher);

            try
            {
                string notesDbPath = NoteSavePath + @"\notes.db";
                if (new FileInfo(notesDbPath).Exists)
                {
                    Package zip = ZipPackage.Open(NoteSavePath + @"\notes.db", FileMode.Open, FileAccess.ReadWrite);
                    this.notes.Clear();
                    PackagePart customPart = zip.GetPart(new Uri("/data", UriKind.Relative));
                    using (Stream s = customPart.GetStream())
                    {
                        BinaryFormatter b = new BinaryFormatter();
                        int nbCount = (int)b.Deserialize(s);
                        for (int i = 0; i < nbCount; ++i)
                        {
                            string nbName = (string)b.Deserialize(s);
                            NoteBook nb = null;
                            foreach (NoteBook loopnb in this.tags)
                            {
                                if (loopnb.NoteBookName.Equals(nbName, StringComparison.InvariantCultureIgnoreCase))
                                {
                                    nb = loopnb;
                                    break;
                                }
                            }
                            if (nb == null)
                            {
                                if (nbName.Equals(TrashFolder.TrashName, StringComparison.OrdinalIgnoreCase))
                                {
                                    nb = new TrashFolder();
                                }
                                else
                                {
                                    nb = new NoteBook(nbName);
                                }
                            }
                            InvokeOC<NoteTag> nbTags = (InvokeOC<NoteTag>)b.Deserialize(s);
                            nb.AddTags(nbTags);
                            this.tags.Add(nb);
                        }

                        int count = (int)b.Deserialize(s);
                        for (int i = 0; i < count; ++i)
                        {
                            Note note = new Note(b, s, this.tags);
                            this.notes.Add(note);
                        }

                        s.Close();
                    }

                    zip.Close();
                }
            }
            catch (System.Exception e)
            {
                TaskDialog.ShowException(e);
            }
        }

        ~NoteList()
        {
            this.Save();
        }

        public static NoteList Instance
        {
            get { return instance; }
        }

        public InvokeOC<Note> Notes
        {
            get { return this.notes; }
        }

        public NoteBook CurrentNotebook
        {
            get
            {
                return this.currentNoteBook;
            }

            set
            {
                if (this.CurrentNotebook != value)
                {
                    this.currentNoteBook = value;
                    this.RaisePropertyChanged("CurrentNotebook");
                    this.RaisePropertyChanged("CurrentTags");
                }
            }
        }

        public InvokeOC<NoteTag> CurrentTags
        {
            get { return this.CurrentNotebook.Tags; }
        }

        public InvokeOC<NoteBook> Tags
        {
            get { return this.tags; }
        }

        public static string GetNotebookDirPath()
        {
            return NoteSavePath;
        }

        /// <summary>
        /// Walks the tags tree and executes the action on every tag
        /// until the action returns true.
        /// </summary>
        /// <param name="tags">The tags.</param>
        /// <param name="action">The action to execute.</param>
        /// <returns>true if the action returned true on one of the tags, false otherwise.</returns>
        public static bool WalkTags(InvokeOC<NoteTag> tags, Func<NoteTag, bool> action)
        {
            foreach (NoteTag t in tags)
            {
                if (action(t))
                {
                    return true;
                }

                if (WalkTags(t.Tags, action))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Walks the tags tree and executes the action on every tag
        /// until the action returns true.
        /// </summary>
        /// <param name="tags">The tags.</param>
        /// <param name="action">The action to execute.</param>
        /// <returns>the NoteTag for which the action returned true or null otherwise.</returns>
        public static NoteTag WalkNoteTags(InvokeOC<NoteTag> tags, Func<NoteTag, bool> action)
        {
            foreach (NoteTag t in tags)
            {
                if (action(t))
                {
                    return t;
                }

                NoteTag tag = WalkNoteTags(t.Tags, action);
                if (tag != null)
                {
                    return tag;
                }
            }

            return null;
        }

        /// <summary>
        /// Walks the tags tree and executes the action on every tag
        /// until the action returns true.
        /// </summary>
        /// <param name="tags">The tags.</param>
        /// <param name="action">The action to execute.</param>
        /// <returns>the NoteTag for which the action returned true or null otherwise.</returns>
        public static NoteTag WalkNoteTagsIndex(InvokeOC<NoteTag> tags, Func<InvokeOC<NoteTag>, int, bool> action)
        {
            for (int i = 0; i < tags.Count; ++i)
            {
                if (action(tags, i))
                {
                    return tags[i];
                }
                NoteTag tag = WalkNoteTagsIndex(tags[i].Tags, action);
                if (tag != null)
                {
                    return tag;
                }
            }
            return null;
        }


        /// <summary>
        /// Finds the given tag in the whole NoteTag tree.
        /// </summary>
        /// <param name="tags">The tags to search in.</param>
        /// <param name="tag">The tag to search for.</param>
        /// <returns>true if the tag was found.</returns>
        public static bool FindTag(InvokeOC<NoteTag> tags, NoteTag tag)
        {
            return WalkTags(tags, (obj) => ((NoteTag)obj).TagPath == tag.TagPath);
        }

        /// <summary>
        /// Removes the given tag from the NoteTag tree.
        /// </summary>
        /// <param name="tags">The tags to remove the tag from.</param>
        /// <param name="tag">The tag to remove.</param>
        /// <returns>true if the tag was removed, false if the tag was not found.</returns>
        public static bool RemoveTag(InvokeOC<NoteTag> tags, NoteTag tag)
        {
            //NoteList.WalkNoteTags(tags, (obj) =>
            //{
            //    ((NoteTag)obj)
            //    return false;
            //});

            foreach (NoteTag t in tags)
            {
                if (t.TagPath.Equals(tag.TagPath, StringComparison.Ordinal))
                {
                    bool ret = tags.Remove(t);
                    Debug.Assert(ret, "could not remove tag!");
                    return ret;
                }

                if (RemoveTag(t.Tags, tag))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Inserts the given tag in the NoteTag tree at the correct location.
        /// Intermediate tags are automatically created.
        /// </summary>
        /// <param name="tags">The tags to insert the tag into.</param>
        /// <param name="tag">The tag to insert.</param>
        /// <returns>true if the tag was inserted or an intermediate tag was created, false if the tree is left unchanged.</returns>
        public static bool AddTag(InvokeOC<NoteTag> tags, NoteTag tag)
        {
            bool ret = false;
            if (!FindTag(tags, tag))
            {
                if (tag.TagName.Length == 0)
                {
                    return ret;
                }

                string partialtag = null;
                string[] splits = tag.TagPath.Split(new char[] { ':' });
                InvokeOC<NoteTag> temptags = tags;
                foreach (string s in splits)
                {
                    partialtag = partialtag != null ? (partialtag + ":" + s) : s;
                    NoteTag ntag = temptags.FirstOrDefault((obj) => ((NoteTag)obj).TagPath.CompareTo(s) == 0);
                    if (ntag == null)
                    {
                        if (partialtag.Equals(tag.TagPath, StringComparison.Ordinal))
                        {
                            ntag = tag;
                        }
                        else
                        {
                            ntag = new NoteTag(partialtag, tag.NoteBook);
                        }

                        temptags.Add(ntag);
                        ret = true;
                    }

                    temptags = ntag.Tags;
                }
            }

            return ret;
        }

        public bool Save()
        {
            bool ret = true;
            try
            {
                Package zip = ZipPackage.Open(NoteSavePath + @"\notes.db", FileMode.Create, FileAccess.ReadWrite);

                PackagePart customPart = zip.CreatePart(new Uri("/data", UriKind.Relative), "application/octet-stream", CompressionOption.Fast);
                using (Stream s = customPart.GetStream())
                {
                    BinaryFormatter b = new BinaryFormatter();
                    b.Serialize(s, this.tags.Count);
                    foreach (NoteBook nbloop in this.tags)
                    {
                        b.Serialize(s, nbloop.NoteBookName);
                        b.Serialize(s, nbloop.Tags);
                    }

                    b.Serialize(s, this.notes.Count);
                    foreach (Note note in this.notes)
                    {
                        note.Serialize(b, s);
                    }

                    s.Close();
                }

                zip.Close();
            }
            catch (System.Exception e)
            {
                System.Diagnostics.Debug.Print(e.Message);
                ret = false;
            }

            return ret;
        }

        public bool AddNewNote(Note note)
        {
            if (this.CurrentNotebook != null)
            {
                this.CurrentNotebook.AddTags(note.Tags);
                note.NoteBook = this.CurrentNotebook;
                this.notes.Add(note);
                this.RaisePropertyChanged("CurrentTags");
                this.RaisePropertyChanged("Tags");
                this.RaisePropertyChanged("Notes");

                return true;
            }

            return false;
        }

        public void AddNote(Note note)
        {
            this.notes.Add(note);
            this.RaisePropertyChanged("CurrentTags");
            this.RaisePropertyChanged("Tags");
            this.RaisePropertyChanged("Notes");
        }

        public bool DeleteNote(Note note)
        {
            try
            {
                File.Delete(note.Path);
                File.Delete(note.DataPath);
                this.notes.Remove(note);
            }
            catch (System.Exception)
            {
                return false;
            }

            return true;
        }

        public void RemoveTag(NoteTag tag)
        {
            foreach (Note note in this.notes)
            {
                // loop through all notes and remove the tag
                if (note.NoteBook == this.currentNoteBook)
                {
                    note.RemoveTag(tag);
                }
            }

            this.currentNoteBook.RemoveTag(tag);
            this.RaisePropertyChanged("CurrentTags");
        }

        /// <summary>
        /// Moves the NoteTag src to the new position of NoteTag dest.
        /// </summary>
        /// <param name="src">the NoteTag to move</param>
        /// <param name="dest">the place to move src to</param>
        /// <param name="inside">if true, src is moved into dest, otherwise it's moved
        /// near dest.</param>
        public void MoveTag(NoteTag src, NoteTag dest, bool inside)
        {
            // if the dest tag has a different path as the source,
            // we have not only to change the list order but also
            // 1. remove the source tag
            // 2. find all notes that have the source tag and remove that tag
            // 3. change the path of the source tag
            // 4. insert the source tag to all notes that had the tag before 2.
            // 5. finally adjust the index in the list of the source tag

            // 1. remove the source tag
            src.NoteBook.RemoveTag(src);

            int srcIndex = src.TagPath.LastIndexOf(":");
            int dstIndex = dest.TagPath.LastIndexOf(":");
            if (inside || ((srcIndex < 0) && (dstIndex >= 0)) ||
                ((srcIndex >= 0) && (dstIndex < 0)) ||
                (((srcIndex > 0) && (dstIndex > 0)) && ((src.TagPath.Substring(srcIndex) != dest.TagPath.Substring(dstIndex)))))
            {
                // 2. find all notes that have the source tag and remove that tag
                List<Note> notesWithSrcTag = new List<Note>();
                foreach (Note n in this.notes)
                {
                    if (FindTag(n.Tags, src))
                    {
                        n.RemoveTag(src);
                        notesWithSrcTag.Add(n);
                    }
                }
                // 3. change the path of the source tag
                int index = dest.TagPath.LastIndexOf(':');
                if (inside)
                {
                    src.TagPath = dest.TagPath + ":" + src.TagName;
                }
                else if (index >= 0)
                {
                    src.TagPath = dest.TagPath.Substring(0, index + 1) + src.TagName;
                }
                else
                {
                    src.TagPath = src.TagName;
                }
                // 4. insert the source tag to all notes that had the tag before 2.
                foreach (Note n in notesWithSrcTag)
                {
                    n.AddTag(src);
                }
            }

            if (inside)
            {
                src.NoteBook.AddTag(src);
            }
            else
            {
                // 5. finally adjust the index in the list of the source tag
                src.NoteBook.RemoveTag(src);
                NoteList.WalkNoteTagsIndex(src.NoteBook.Tags, (obj, index) =>
                {
                    if (((InvokeOC<NoteTag>)obj)[index] == dest)
                    {
                        ((InvokeOC<NoteTag>)obj).Insert(index + 1, src);
                        return true;
                    }
                    return false;
                });
            }
        }

        public bool MoveNoteToTrash(Note note)
        {
            bool ret = true;
            string filename = System.IO.Path.GetFileName(note.Path);
            string newPath = NoteSavePath + "\\" + TrashFolder.TrashName + "\\" + filename;
            string newDataPath = NoteSavePath + "\\" + TrashFolder.TrashName + "\\" + System.IO.Path.GetFileName(note.DataPath);
            try
            {
                Directory.CreateDirectory(NoteSavePath + "\\" + TrashFolder.TrashName);
            }
            catch (System.Exception)
            {
            }

            try
            {
                if (File.Exists(newPath))
                {
                    File.Delete(newPath);
                }
                if (File.Exists(newDataPath))
                {
                    File.Delete(newDataPath);
                }

                if (File.Exists(note.Path))
                    File.Move(note.Path, newPath);
                if (File.Exists(note.DataPath))
                    File.Move(note.DataPath, newDataPath);
                note.Path = newPath;

                // now make sure the "Trash" Notebook exists
                bool foundTrashBook = false;
                foreach (NoteBook book in this.tags)
                {
                    if (book.NoteBookName.Equals("Trash", StringComparison.Ordinal))
                    {
                        foundTrashBook = true;
                        note.NoteBook = book;
                        break;
                    }
                }

                if (!foundTrashBook)
                {
                    // create a new notebook for trash
                    TrashFolder trashBook = new TrashFolder();
                    note.NoteBook = trashBook;
                }

                this.RaisePropertyChanged("Notes");
            }
            catch (System.Exception)
            {
                ret = false;
            }

            return ret;
        }

        public Note CopyNoteToNotebook(Note note, NoteBook noteBook)
        {
            string filename = System.IO.Path.GetFileName(note.Path);
            string newPath = NoteSavePath + "\\" + noteBook.NoteBookName + @"\" + System.Guid.NewGuid().ToString() + @".skn";
            string newDataPath = newPath.Replace(@".skn", @".skd");

            File.Copy(note.Path, newPath, true);
            File.Copy(note.DataPath, newDataPath, true);
            Note newNote = new Note(newPath, noteBook);
            // remove all tags from the note
            newNote.Tags.Clear();
            newNote.SaveNoteData();

            this.notes.Add(newNote);

            this.RaisePropertyChanged("Notes");

            return newNote;
        }

        public bool CreateNotebook(string noteBookName)
        {
            if (Directory.Exists(NoteSavePath + "\\" + noteBookName))
                return false;   // notebook already exists

            NoteBook noteBook = new NoteBook(noteBookName);

            Tags.Add(noteBook);
            // create the directory for the notebook
            Directory.CreateDirectory(NoteSavePath + "\\" + noteBookName);

            if (CurrentNotebook == null)
            {
                CurrentNotebook = noteBook;
            }
            return true;
        }
    }
}
